package server;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static server.MessageClass.*;

// unbekannt (mind. 2h)
// 19.00 - 0.00 (5h)
// 11.00 - 19:20 /8h 20min
/* "TODO":
  1. Socket und PrintWriter in einer Hilfsklasse verwalten (Konrad)
  9. MOTD Gruß (Michael) KOMMENTAR: Ich habe keine Lust mehr gehabt. Aber in der Theorie: Eine TXT oder Teil aus der JSON, wird bei jeder init() im Thread aufgerufen.
  b. Zitieren (Jan) KOMMENTAR: Das ist besonders unnütz in so einer Umgebung, da man in IRCs normalerweise mit @Nutzername sich an jemanden richtet. Eine Formatierung ist eigentlich *nicht* möglich.
  c. Sprach-Filter (Jan) KOMMENTAR: Ja, das ließe sich zwar relativ einfach einbauen (man parse eine JSON mit Beleidigungen ein und kontrolliere bei else jeden Input im Vergleich mit dieser Liste), aber das wäre für mich keine Ambition. Soll jeder schreiben, was er will.
  d. Timeout für unflätige Nutzer: Siehe voriges.
  e. Gruppen (Konrad) KOMMENTAR: Für jeden Server einen Raum -- Mehr braucht es nicht.
  f. Protokoll als TXT-Datei KOMMENTAR: Wäre auch kein Aufwand, nur was Ressourcen(!) angeht ist es störend: Man möge einen weiteren Outputstream erstellen und bei jeder Methode und Nachricht neben flush() auch noch in diese Datei schreiben.
*/
/*
    ZUSÄTZLICHE FEATURES:
        - Hilfsklasse für Input allgemein
        - Dynamische Integration von Strings
        - Unterschiedliche Sprachen, die jeder Thread selbst auswählen kann
        - Strings werden aus einer JSON-Datei geparsed
        - whisper-Funktion (mit der Intention, nichts zurückzugeben; man denke nach, was man sende)
 */

public class TCPServer {
    // Color escape sequences for indicating different message and event types
    public static final String ANSI_RESET = "\u001B[0m";
    //public static final String ANSI_BLACK = "\u001B[30m";
    public static final String ANSI_RED = "\u001B[31m";
    public static final String ANSI_GREEN = "\u001B[32m";
    public static final String ANSI_YELLOW = "\u001B[33m";
    public static final String ANSI_BLUE = "\u001B[34m";
    //public static final String ANSI_PURPLE = "\u001B[35m";
    public static final String ANSI_CYAN = "\u001B[36m";
    //public static final String ANSI_WHITE = "\u001B[37m";

    private ServerSocket serverSocket;
    private ArrayList<ClientThread> users = new ArrayList<>();
    private Calendar dt = Calendar.getInstance();

    public TCPServer(int port) throws Exception {
        // TODO: Behandeln Sie das Problem, dass die Socket nicht geöffnet werden kann
        serverSocket = new ServerSocket(port);
        System.out.println("Listening on " + serverSocket + " on Port " + port);

        listen();
    }

    static public void main(String[] args) {
        int port = 3000;
        try {
            new TCPServer(port);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    private void listen() throws Exception {
        boolean running = true;
        while (running) {
            // TODO: Kümmern Sie sich um die Exception
            Socket s = serverSocket.accept();
            System.out.println("Connection from " + s);
            ClientThread user = new ClientThread(this, s);
            users.add(user);
            user.start();
        }
    }

    void broadcast(ClientThread trigger, eventType eventType, String... args) throws Exception {
        switch (eventType) {
            case JOIN:
                for (ClientThread user : users) {
                    user.send(ANSI_GREEN + trigger.getUsername() + request(RequestType.USER_JOINED, user.getLanguage(), ANSI_GREEN));
                }
                break;
            case LEFT:
                for (ClientThread user : users) {
                    user.send(ANSI_GREEN + trigger.getUsername() + request(RequestType.USER_LEFT, user.getLanguage(), ANSI_GREEN));
                }
                break;
            case STATUS:
                for (ClientThread user : users) {
                    try {
                        user.send(ANSI_BLUE + trigger.getUsername() + request(MessageClass.RequestType.valueOf(args[0].toUpperCase()), user.getLanguage(), ANSI_BLUE));
                    } catch (IllegalArgumentException iea) {
                        trigger.send(request(RequestType.STATUS_DOES_NOT_EXIST, user.getLanguage(), ANSI_RED));
                    }
                }
                break;
            default:
                System.out.println("debug");
                break;
        }

    }

    void sendToAll(ClientThread sender, String message) {
        for (ClientThread user : users) {
            if (user != sender) {
                user.send(ANSI_YELLOW + messageFormat(message, sender.getUsername()) + ANSI_RESET);
            }
        }
    }

    synchronized void whisper(ClientThread sender, String recipent, String message) throws Exception {
        for (ClientThread user : users) {
            if (user.getUsername().equals(recipent) && !(user.getUsername().equals(sender.getUsername()))) {
                user.send(ANSI_CYAN + messageFormat(message, sender.getUsername()) + ANSI_RESET);
                sender.send(request(MessageClass.RequestType.WHISPER_SENT, sender.getLanguage(), ANSI_YELLOW));
            }
        }
    }

    synchronized String listMembers() {
        StringBuilder out = new StringBuilder();
        for (ClientThread user : users) {
            out.append("\n").append(user.getUsername());
        }
        return out.toString();
    }

    public String messageFormat(String message, String username) {
        //System.out.println("Nachricht: " + formatMessage);
        return prettyTime() + " <" + username + "> " + message;
    }

    public boolean isUsernameTaken(String name) {
        for (ClientThread user : users) {
            try {
                if (user.getUsername().equals(name)) {
                    return true;
                }
            } catch (NullPointerException n) {
                return false;
            }
        }
        return false;
    }

    public void command(ClientThread sender, String command, String args, String argv) throws Exception {
        switch (command) {
            case "help":
                sender.send(MessageClass.request(MessageClass.RequestType.HELP, sender.getLanguage(), ANSI_CYAN));
                break;
            case "whisper":
                whisper(sender, args, argv);
                break;
            case "language":
                sender.setLanguage(langStringToType(args));
                sender.send(MessageClass.request(MessageClass.RequestType.LANG, sender.getLanguage(), ANSI_CYAN));
                break;
            case "exit":
                broadcast(sender, eventType.LEFT);
                removeConnection(sender.getSocket(), sender.getPrintWriter());
                sender.endThread();
                break;
            case "list":
                sender.send(listMembers());
                break;
            case "status":
                broadcast(sender, eventType.STATUS, args, argv);
                break;
            default:
                sender.send(MessageClass.request(MessageClass.RequestType.UNKNOWN, sender.getLanguage(), ANSI_RED));
                break;
        }
    }

    public String prettyTime() {
        String prettyTime;
        String hourS;
        String minS;
        int hour = dt.get(Calendar.HOUR_OF_DAY);
        int min = dt.get(Calendar.MINUTE);
        if (min <= 9) {
            minS = "0" + min;
        } else {
            minS = Integer.toString(min);
        }

        if (hour <= 9) {
            hourS = "0" + hour;
        } else {
            hourS = Integer.toString(hour);
        }

        prettyTime = hourS + ":" + minS;

        return prettyTime;
    }

    void removeConnection(Socket s, PrintWriter sender) {
        synchronized (sender) {
            sender.close();
            try {
                s.close();
            } catch (IOException ie) {
                ie.printStackTrace();
            }
        }
    }

    public enum eventType {
        JOIN, LEFT, STATUS
    }
}

class ClientThread extends Thread {
    private TCPServer server;
    private Socket socket;
    private Scanner din;
    private PrintWriter dout;
    private String username;
    private MessageClass.Language language;

    // Important for thread management, due to that stop() is deprecated.
    private volatile boolean exit;

    public ClientThread(TCPServer server, Socket socket) {
        this.server = server;
        this.socket = socket;
        this.exit = false;

        try {
            din = new Scanner(socket.getInputStream());
            dout = new PrintWriter(socket.getOutputStream());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void send(String message) {
        dout.println(message);
        dout.flush();
    }

    public void run() {
        try {
            try {
                init();
            } catch (Exception e) {
                e.printStackTrace();
            }
            while (!exit) {
                // Input logic is set here.
                String message = din.nextLine();
                Pattern pattern = Pattern.compile("^/(\\w*)\\s?(\\w*)?\\s?(.*)?");
                Matcher matcher = pattern.matcher(message);
                if (matcher.find()) {
                    try {
                        server.command(this, matcher.group(1), matcher.group(2), matcher.group(3));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                } else {
                    // Implicates that we received a "normal" message.
                    server.sendToAll(this, message);
                }
            }
        } finally {
            server.removeConnection(socket, this.dout);
        }
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void init() throws Exception {
        boolean nameSet = false;
        boolean langSet = false;

        do {
            send("Enter your language/Gebe deine Sprache ein/Entrer ta langue [EN/DE/FR]");
            String in = din.nextLine();
            if (!in.equals("")) {
                setLanguage(langStringToType(in));
                send(MessageClass.request(MessageClass.RequestType.LANG, this.language, TCPServer.ANSI_YELLOW));
                langSet = true;
            }
        } while (!langSet);

        do {
            send(MessageClass.request(MessageClass.RequestType.USERNAME_GET, this.language, TCPServer.ANSI_YELLOW));
            String in = din.nextLine();
            if (!in.matches("^([\\w\\-]+)") || in.equals("")) {
                send(MessageClass.request(MessageClass.RequestType.INVALID_NAME, this.language, TCPServer.ANSI_RED));
            } else {
                if (server.isUsernameTaken(in)) {
                    send(MessageClass.request(RequestType.TAKEN_NAME, this.language, TCPServer.ANSI_RED));
                } else {
                    setUsername(in);
                    send(MessageClass.request(MessageClass.RequestType.USERNAME_SET, this.language, TCPServer.ANSI_GREEN) + username);
                    nameSet = true;
                }
            }
        } while (!nameSet);
        server.broadcast(this, TCPServer.eventType.JOIN);
    }

    public MessageClass.Language getLanguage() {
        return this.language;
    }

    public void setLanguage(MessageClass.Language language) {
        this.language = language;
    }

    public Socket getSocket() {
        return this.socket;
    }

    public PrintWriter getPrintWriter() {
        return this.dout;
    }

    public void endThread() {
        this.exit = true;
    }

}